iT邦幫忙

2026 iThome 鐵人賽

0
AI Engineering

Java × AI:30天打造智慧健康助理系列 第 18 篇

用 Java 建立睡眠時間紀錄功能

  • 分享至 

  • xImage
  •  

Day 18|用 Java 建立睡眠時間紀錄功能

昨天的 Day 17,我們成功讓智慧健康助理加入七天飲水量統計功能!

現在系統不只可以新增飲水紀錄,還能查詢歷史資料、計算平均值,以及找出最高和最低的紀錄量。

不過仔細想想,既然我們的目標是打造「智慧健康助理」,好像不能一直只圍繞著飲水量打轉 XD

所以今天決定挑戰第二種日常健康紀錄:

睡眠時間紀錄!

這次要讓使用者可以輸入每天睡了幾個小時,並將資料保存到 H2 Database。

今天的目標是:

使用 Spring Boot、DTO、Repository 和 H2 Database,建立睡眠時間紀錄與查詢 API。


一、為什麼選擇睡眠紀錄?

在一開始規劃智慧健康助理時,我希望系統可以逐漸整合不同種類的健康資訊。

目前已經完成:

  • BMI 計算與分類
  • 每日飲水量紀錄
  • 飲水量歷史查詢
  • 七天飲水量統計

接下來加入睡眠紀錄,就能讓系統開始管理第二種日常健康資料。

例如:

10/15 → 睡眠 7.5 小時
10/16 → 睡眠 6 小時
10/17 → 睡眠 8 小時

未來就可以進一步分析:

最近七天平均睡眠時間

甚至結合飲水紀錄,產生簡單的生活習慣摘要。

不過今天先不急著做統計,也不會讓 AI 分析睡眠品質。

先完成資料的接收、驗證、儲存與查詢。


二、今天要完成哪些功能?

今天準備新增兩個 API:

HTTP Method API 功能
POST /health/sleep 新增睡眠紀錄
GET /health/sleep 查詢指定日期的睡眠紀錄

例如使用者傳送:

{
  "sleepDate": "2026-10-17",
  "hours": 7.5
}

系統就會將這筆資料保存到資料庫。

成功後預期回傳:

{
  "message": "Sleep record saved successfully.",
  "id": 1,
  "sleepDate": "2026-10-17",
  "hours": 7.5
}

之後如果想查詢某一天的睡眠紀錄,就可以使用:

GET /health/sleep?date=2026-10-17

這樣就不用每次都進入 H2 Console 手動查詢資料了。


三、先決定睡眠資料要怎麼設計

在寫程式之前,我們先想一下睡眠紀錄需要哪些欄位。

目前先使用最簡單的設計:

欄位 資料型別 說明
id BIGINT 每筆紀錄的編號
sleep_date DATE 睡眠紀錄所屬日期
hours DOUBLE 睡眠時數

例如:

id sleep_date hours
1 2026-10-15 7.5
2 2026-10-16 6.0
3 2026-10-17 8.0

這裡有一個需要先決定的問題:

如果睡眠時間跨越兩天,日期要算哪一天?

例如:

10/16 晚上 23:30 入睡
10/17 早上 07:00 起床

今天先約定:

sleepDate 代表起床當天的日期。

因此這筆睡眠紀錄會記在:

2026-10-17

另外,今天只記錄睡眠總時數,不記錄實際入睡和起床時間。

這樣可以先把資料庫和 API 功能完成,之後再逐步擴充。


四、建立 SleepRequest DTO

首先建立新的 DTO:

SleepRequest.java

放在:

dto

Package 裡面。

完整程式如下:

package com.example.healthassistant.dto;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.DecimalMax;

import java.time.LocalDate;

public class SleepRequest {

    @NotNull(message = "Sleep date is required.")
    private LocalDate sleepDate;

    @NotNull(message = "Sleep hours are required.")
    @DecimalMin(value = "0.0", inclusive = false,
            message = "Sleep hours must be greater than 0.")
    @DecimalMax(value = "24.0",
            message = "Sleep hours must not exceed 24.")
    private Double hours;

    public SleepRequest() {
    }

    public LocalDate getSleepDate() {
        return sleepDate;
    }

    public void setSleepDate(LocalDate sleepDate) {
        this.sleepDate = sleepDate;
    }

    public Double getHours() {
        return hours;
    }

    public void setHours(Double hours) {
        this.hours = hours;
    }
}

這裡使用了之前 Day 11 學過的 Bean Validation。

@NotNull

@NotNull

代表欄位不能沒有值。

例如:

{
  "hours": 7.5
}

因為沒有提供日期,就不符合我們的資料格式。

@DecimalMin

@DecimalMin(value = "0.0", inclusive = false)

代表睡眠時數必須大於 0。

例如:

{
  "sleepDate": "2026-10-17",
  "hours": -3
}

就不應該被接受。

@DecimalMax

@DecimalMax(value = "24.0")

代表我們暫時將單筆睡眠時數限制在 24 小時以內。

這只是今天練習使用的輸入範圍,並不是用來判斷睡眠是否健康。


五、在 H2 建立睡眠紀錄資料表

接下來要讓資料庫可以保存睡眠資料。

打開之前建立的:

src/main/resources/schema.sql

保留原本的 water_records 資料表,並在下方新增:

CREATE TABLE IF NOT EXISTS sleep_records (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    sleep_date DATE NOT NULL,
    hours DOUBLE PRECISION NOT NULL,
    CONSTRAINT chk_sleep_hours
        CHECK (hours > 0 AND hours <= 24)
);

這樣資料庫就會有兩張主要資料表:

water_records
sleep_records

分別負責儲存:

飲水紀錄
睡眠紀錄

因為昨天已經設定:

spring.sql.init.mode=always

所以 Spring Boot 啟動時會執行 schema.sql。

這裡使用:

CREATE TABLE IF NOT EXISTS

因此資料表已經存在時,就不會重複建立。

不過這不代表資料表結構會自動更新。未來如果修改欄位,還是需要另外處理資料庫結構變更。


六、為什麼睡眠紀錄要建立新的 Repository?

前幾天我們已經建立:

WaterRepository.java

負責操作飲水紀錄。

今天則要新增:

SleepRepository.java

因為飲水量和睡眠時間是不同種類的資料。

如果全部寫在同一個 Repository 裡面,未來功能越來越多時,程式會變得很難管理。

所以今天繼續維持之前的分層架構:

SleepController
       ↓
SleepService
       ↓
SleepRepository
       ↓
H2 Database

每一層都有自己的工作。


七、建立 SleepRepository

接下來建立:

SleepRepository.java

放在:

repository

Package 裡面。

完整程式如下:

package com.example.healthassistant.repository;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.List;

import com.example.healthassistant.dto.SleepResponse;

@Repository
public class SleepRepository {

    private final JdbcTemplate jdbcTemplate;

    public SleepRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void save(LocalDate date, double hours) {

        String sql = """
            INSERT INTO sleep_records (sleep_date, hours)
            VALUES (?, ?)
            """;

        jdbcTemplate.update(sql, date, hours);
    }

    public List<SleepResponse> findByDate(LocalDate date) {

        String sql = """
            SELECT id, sleep_date, hours
            FROM sleep_records
            WHERE sleep_date = ?
            ORDER BY id
            """;

        return jdbcTemplate.query(
            sql,
            (rs, rowNum) -> new SleepResponse(
                rs.getLong("id"),
                rs.getDate("sleep_date").toLocalDate(),
                rs.getDouble("hours")
            ),
            date
        );
    }
}

這裡使用了兩個之前學過的 JdbcTemplate Method。

update()

jdbcTemplate.update(sql, date, hours);

負責新增睡眠紀錄。

query()

jdbcTemplate.query(...)

負責查詢資料庫中的睡眠紀錄。

和之前的飲水量歷史查詢一樣,我們使用 query() 取得多筆資料。

因為今天暫時允許同一天新增多筆睡眠紀錄,所以查詢結果使用:

List<SleepResponse>

八、建立 SleepResponse DTO

剛剛的 Repository 使用到:

SleepResponse

所以接下來建立:

SleepResponse.java

程式如下:

package com.example.healthassistant.dto;

import java.time.LocalDate;

public class SleepResponse {

    private Long id;
    private LocalDate sleepDate;
    private double hours;

    public SleepResponse(Long id, LocalDate sleepDate, double hours) {
        this.id = id;
        this.sleepDate = sleepDate;
        this.hours = hours;
    }

    public Long getId() {
        return id;
    }

    public LocalDate getSleepDate() {
        return sleepDate;
    }

    public double getHours() {
        return hours;
    }
}

這個 DTO 用來表示資料庫查詢後的睡眠紀錄。

例如:

{
  "id": 1,
  "sleepDate": "2026-10-17",
  "hours": 7.5
}

和 SleepRequest 不同的是:

SleepRequest 用來接收使用者輸入。

SleepResponse 則用來回傳已儲存的紀錄。

這樣可以讓資料輸入和輸出有比較清楚的區分。


九、建立 SleepService

Repository 完成後,接下來建立 Service。

檔案名稱:

SleepService.java

完整程式如下:

package com.example.healthassistant.service;

import com.example.healthassistant.dto.SleepRequest;
import com.example.healthassistant.dto.SleepResponse;
import com.example.healthassistant.repository.SleepRepository;

import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;

@Service
public class SleepService {

    private final SleepRepository sleepRepository;

    public SleepService(SleepRepository sleepRepository) {
        this.sleepRepository = sleepRepository;
    }

    public SleepResponse addSleep(SleepRequest request) {

        sleepRepository.save(
            request.getSleepDate(),
            request.getHours()
        );

        List<SleepResponse> records =
                sleepRepository.findByDate(request.getSleepDate());

        return records.get(records.size() - 1);
    }

    public List<SleepResponse> getSleepByDate(LocalDate date) {

        return sleepRepository.findByDate(date);
    }
}

這裡有兩個主要 Method。

addSleep()

負責新增睡眠紀錄。

sleepRepository.save(
    request.getSleepDate(),
    request.getHours()
);

會呼叫 Repository,將資料寫入 H2。

getSleepByDate()

負責查詢指定日期的睡眠紀錄。

return sleepRepository.findByDate(date);

會回傳符合日期的資料。

這次先採用比較容易理解的方式:新增後再查詢該日期的紀錄,取得最後一筆作為回傳結果。

不過這個做法在多人同時新增資料時,可能取得別人剛剛新增的紀錄。正式系統應改用資料庫產生的 ID,精確取得本次新增的資料。

目前我們先以單人本機練習為主。


十、建立 SleepController

接下來終於要建立 API 了!

新增:

SleepController.java

完整程式如下:

package com.example.healthassistant.controller;

import com.example.healthassistant.dto.SleepRequest;
import com.example.healthassistant.dto.SleepResponse;
import com.example.healthassistant.service.SleepService;

import jakarta.validation.Valid;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/health/sleep")
public class SleepController {

    private final SleepService sleepService;

    public SleepController(SleepService sleepService) {
        this.sleepService = sleepService;
    }

    @PostMapping
    public ResponseEntity<Map<String, Object>> addSleep(
            @Valid @RequestBody SleepRequest request) {

        SleepResponse saved = sleepService.addSleep(request);

        Map<String, Object> result = new HashMap<>();

        result.put("message", "Sleep record saved successfully.");
        result.put("id", saved.getId());
        result.put("sleepDate", saved.getSleepDate());
        result.put("hours", saved.getHours());

        return ResponseEntity.ok(result);
    }

    @GetMapping
    public ResponseEntity<List<SleepResponse>> getSleep(
            @RequestParam
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
            LocalDate date) {

        List<SleepResponse> records =
                sleepService.getSleepByDate(date);

        return ResponseEntity.ok(records);
    }
}

這次的 Controller 和之前的 WaterController 很相似。

因為我們已經熟悉:

@RestController
@RequestMapping
@PostMapping
@GetMapping

以及:

@Valid

所以建立新的 API 其實沒有想像中困難。

這也讓我發現,之前學過的 Spring Boot 架構可以一直重複使用。

只要資料類型不同,就建立對應的 Controller、Service、Repository 和 DTO。


十一、使用 Postman 測試新增睡眠紀錄

接下來重新啟動 Spring Boot。

先測試新增資料。

HTTP Method:

POST

URL:

http://localhost:8080/health/sleep

Body 選擇:

raw → JSON

輸入:

{
  "sleepDate": "2026-10-17",
  "hours": 7.5
}

如果資料成功儲存,預期回傳類似:

{
  "message": "Sleep record saved successfully.",
  "id": 1,
  "sleepDate": "2026-10-17",
  "hours": 7.5
}

這裡的 id 由資料庫產生,所以實際數字可能不同。

接著再新增另一筆:

{
  "sleepDate": "2026-10-16",
  "hours": 6.0
}

預期就會新增另一筆睡眠紀錄。


十二、使用 GET 查詢睡眠紀錄

接下來測試查詢功能。

HTTP Method:

GET

URL:

http://localhost:8080/health/sleep?date=2026-10-17

如果剛剛已經新增:

{
  "sleepDate": "2026-10-17",
  "hours": 7.5
}

預期回傳:

[
  {
    "id": 1,
    "sleepDate": "2026-10-17",
    "hours": 7.5
  }
]

如果同一天有多筆紀錄,就會回傳多筆資料。

例如:

[
  {
    "id": 1,
    "sleepDate": "2026-10-17",
    "hours": 7.5
  },
  {
    "id": 3,
    "sleepDate": "2026-10-17",
    "hours": 1.0
  }
]

這樣就完成睡眠紀錄的新增與查詢!


十三、測試錯誤的輸入資料

除了測試正常資料,也要確認輸入驗證有沒有發揮作用。

情況一:睡眠時數為負數

{
  "sleepDate": "2026-10-17",
  "hours": -2
}

因為我們設定:

@DecimalMin(value = "0.0", inclusive = false)

所以這筆資料應該無法通過驗證。

情況二:沒有輸入睡眠日期

{
  "hours": 7.5
}

因為:

@NotNull

所以也不應該被接受。

情況三:睡眠時數超過 24

{
  "sleepDate": "2026-10-17",
  "hours": 30
}

同樣會因為:

@DecimalMax(value = "24.0")

而驗證失敗。

如果前幾天建立的 GlobalExceptionHandler 已經正確處理 Bean Validation 錯誤,就可以回傳對應的錯誤資訊。

這樣能減少不合理資料進入資料庫的機會。


十四、確認資料真的存進 H2

和之前的飲水紀錄一樣,我們可以進入 H2 Console 確認資料。

網址:

http://localhost:8080/h2-console

連接資料庫後,輸入:

SELECT * FROM sleep_records;

預期可以看到:

ID SLEEP_DATE HOURS
1 2026-10-17 7.5
2 2026-10-16 6.0

如果想依照日期排序,也可以使用:

SELECT *
FROM sleep_records
ORDER BY sleep_date;

這樣就能確認睡眠紀錄真的已經保存到資料庫。


十五、目前的專案結構

今天新增了睡眠紀錄相關的 Controller、Service、Repository 和 DTO。

目前專案結構如下:

health-assistant
│
├── src/main/java/com/example/healthassistant
│   │
│   ├── HealthAssistantApplication.java
│   │
│   ├── controller
│   │   ├── HealthController.java
│   │   ├── WaterController.java
│   │   └── SleepController.java
│   │
│   ├── service
│   │   ├── BmiService.java
│   │   ├── WaterService.java
│   │   └── SleepService.java
│   │
│   ├── repository
│   │   ├── WaterRepository.java
│   │   └── SleepRepository.java
│   │
│   ├── dto
│   │   ├── HealthRequest.java
│   │   ├── WaterRequest.java
│   │   ├── WaterHistoryResponse.java
│   │   ├── WaterStatisticsResponse.java
│   │   ├── SleepRequest.java
│   │   └── SleepResponse.java
│   │
│   └── exception
│       └── GlobalExceptionHandler.java
│
├── src/main/resources
│   ├── application.properties
│   └── schema.sql
│
└── pom.xml

現在的智慧健康助理已經有兩組資料庫功能:

飲水紀錄
    ↓
WaterController
    ↓
WaterService
    ↓
WaterRepository
    ↓
water_records

以及:

睡眠紀錄
    ↓
SleepController
    ↓
SleepService
    ↓
SleepRepository
    ↓
sleep_records

這樣的架構也讓不同健康功能可以獨立管理。


十六、目前的睡眠紀錄還有哪些限制?

雖然今天成功建立睡眠紀錄功能,但目前仍然是比較簡單的版本。

1. 只記錄睡眠總時數

目前只輸入:

7.5 小時

但沒有記錄:

幾點入睡
幾點起床

所以還無法分析睡眠時間分布。

2. 同一天可以有多筆紀錄

目前資料庫沒有設定同一天只能有一筆紀錄。

如果使用者重複新增,資料庫就會保存多筆。

未來可以再決定是否要支援午睡、分段睡眠,或限制每天只能有一筆主要睡眠紀錄。

3. 還沒有統計功能

目前只能新增與查詢。

還不能直接計算:

最近七天平均睡眠時間

這個功能可以留到下一篇繼續完成。

4. 還沒有使用者帳號

和飲水紀錄一樣,目前所有資料都放在同一個資料庫裡,沒有區分不同使用者。

因此仍然只適合單人本機練習。


十七、睡眠紀錄和 AI 有什麼關係?

今天雖然還沒有使用 AI,但我們已經開始建立第二種健康資料。

目前系統可以記錄:

飲水量
睡眠時間

未來如果再加入運動紀錄,就能整理出更完整的日常生活資訊。

例如:

{
  "date": "2026-10-17",
  "waterIntake": 1500,
  "sleepHours": 7.5,
  "exerciseMinutes": 30
}

之後可以先由 Java 計算各種統計數值,再讓 AI 協助產生文字摘要。

例如:

今天有記錄飲水量、睡眠時間及運動時間。
可以透過每週紀錄觀察生活習慣的變化。

不過要注意,睡眠時間不等於睡眠品質。

單純記錄睡了幾個小時,也無法直接判斷使用者是否有睡眠障礙。

所以目前的智慧健康助理仍然以資料紀錄和一般資訊整理為主,不會提供醫療診斷。


Day 18 小結

今天成功讓智慧健康助理加入第二種日常健康紀錄!

除了飲水量之外,現在也可以記錄睡眠時間。

今天完成的功能包括:

  1. 建立 SleepRequest DTO
  2. 使用 Bean Validation 驗證睡眠資料
  3. 建立 sleep_records 資料表
  4. 建立 SleepRepository
  5. 使用 JdbcTemplate 新增與查詢資料
  6. 建立 SleepService
  7. 建立 SleepController
  8. 使用 POST 和 GET API 操作睡眠紀錄

目前智慧健康助理已經具備:

BMI 計算與分類
       │
       ├── 飲水量紀錄
       │       ├── 新增紀錄
       │       ├── 歷史查詢
       │       └── 七天統計
       │
       └── 睡眠時間紀錄
               ├── 新增紀錄
               └── 指定日期查詢

今天最大的收穫是:

學會將之前建立的 Spring Boot 分層架構,應用到不同種類的健康資料上。

原本以為每增加一個功能,都需要重新學習很多東西。

但實際整理後發現,Controller、Service、Repository 和 DTO 的概念都可以繼續沿用。

只要先把資料結構和功能需求想清楚,就能一步一步擴充系統。


明日預告

Day 19|睡眠紀錄也能分析!建立七天平均睡眠時間 API

今天已經成功建立睡眠時間紀錄功能。

但目前只能新增資料和查詢指定日期。

所以下一篇準備加入睡眠統計功能!

預計會學習:

  • 使用 SQL 查詢最近七天的睡眠紀錄
  • 使用 Java 計算平均睡眠時間
  • 找出睡眠紀錄最長與最短的日期
  • 建立睡眠統計 DTO
  • 建立睡眠統計 API

讓智慧健康助理不只可以保存睡眠紀錄,也能開始整理睡眠時間的變化!


上一篇
用 Java 分析七天飲水量
下一篇
用 Java 建立七天睡眠統計 API
系列文
Java × AI:30天打造智慧健康助理 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言